package jupp;

import java.io.*;
import java.util.*;
import java.util.prefs.*;

/**
* JuppMap offers persistence to serializable objects using java.util.prefs.Preferences
* as a platform-independent backend.
*
* <p>
* JuppMap is born out of a little idea I had while observing the multitude of
* people who appeared to be planning to save simple application data to a
* platform-dependent location using java.io.File, and asking how to do it at
* javaprogrammingforums.com - I wrote a little article about the idea there:
* <a href="http://www.javaprogrammingforums.com/file-input-output-tutorials/10459-look-ma-no-files-portable-object-persistence.html">Look ma, no files! Portable object persistence</a>
* and noticed soon after an older IBM article on the same subject here:
* <a href="http://www.ibm.com/developerworks/java/library/j-prefapi/index.html">Store objects using the Preferences API</a>
* </p>
* <p>It's worth pointing out that Sun deliberately omitted Object support from
* the Preferences subsystem. Here's the relevant quote from their
* <a href="http://download.oracle.com/javase/1.4.2/docs/guide/lang/preferences.html#prefs-design-faq">Preferences API Design FAQ</a>:</p>
* <blockquote><b>Why doesn't this API contain methods to read and write
arbitrary serializable objects?</b>

<p>Serialized objects are somewhat fragile: if the version of the
program that reads such a property differs from the version that wrote
it, the object may not deserialize properly (or at all).  It is not
impossible to store serialized objects using this API, but
we do not encourage it, and have not provided a convenience method.

</p>
* </blockquote>
* <p>
* JuppMap does not extend java.util.AbstractMap because it is merely a proxy for
* java.util.prefs.Preferences. Many of the methods (such as get and put) require
* not only object serialization / deserialization, but also
* changes to be written through to Preference's underlying storage mechanism.
* Those back-end changes throw IOExceptions and BackingStoreExceptions, and handling
* those in any meaningful way - even were it possible - would make JuppMap a much
* more complicated proposition.
* </p>
*/
public class JuppMap<V>
{
	private final static String MAP_NAME_PREFIX = "jupp.map.name.";
	private final static int MAX_VALUE_LENGTH = Preferences.MAX_VALUE_LENGTH * 3 / 4;
	private final static Preferences PREFS = Preferences.userNodeForPackage(JuppMap.class);
	private final Preferences NODE;
	/**
	 * Creates a new JuppMap which can retrieve and store objects of the
	 * specified type.
	 <p>
	 * If there is an existing JuppMap
	 * of this mapName, any values stored in it will be available. If no
	 * existing JuppMap is found, an empty JuppMap is created.
	 </p>


	 @param mapName a key which can be used to retrieve this JuppMap in future

	 */
	public JuppMap(String mapName) throws IllegalStateException, BackingStoreException
	{
		try
		{
			NODE = PREFS.node(MAP_NAME_PREFIX + mapName);
			NODE.put("JuppMap.valid", "true");
			NODE.remove("JuppMap.valid");
			NODE.sync();
		}
		catch (Exception e)
		{
			throw new IllegalStateException(JuppMap.class.getName() + " backend has a problem", e);
		}
	}
	/**
	* Returns the previously put object from this JuppMap, or null if no such object is found.

	@param key The key under which a stored object was previously put

	@return The object previously stored on this key or null if no such key exists

	@see #put(String, Object)
	*/
	public V get(String key) throws IOException, ClassNotFoundException, BackingStoreException
	{
		byte[] prefsValue = NODE.getByteArray(key, null);
		if (prefsValue != null)
		{
@SuppressWarnings("unchecked")
			V found = (V)new ObjectInputStream(new ByteArrayInputStream(prefsValue)).readObject();
			return found;
		}
		// check for large node
		if (!NODE.nodeExists(key))
			return null;
		Preferences child = NODE.node(key);
		String[] childKeys = child.keys();
		if (childKeys.length < 1)
			return null; // TODO - exception?
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		for (int i = 0; i < childKeys.length; i++)
		{
			byte[] ab = child.getByteArray(Integer.toString(i), null);
			if (ab == null)
				throw new IllegalStateException("Missing byte array at " + i + " in '" + key + "'");
			baos.write(ab);
		}
@SuppressWarnings("unchecked")
		V found = (V)new ObjectInputStream(new ByteArrayInputStream(getBytes(baos.toByteArray()))).readObject();
		return found;
	}
	/**
	* Removes the previously put object from this JuppMap.

	@param key The key under which the stored object was previously put

	@return The object removed from this JuppMap or null if no such key exists

	@see #put(String, Object)
	*/
	public V remove(String key) throws IOException, ClassNotFoundException, BackingStoreException
	{
		V oldValue = get(key);
		NODE.remove(key);
		if (NODE.nodeExists(key))
			NODE.node(key).removeNode();
		NODE.sync();
		return oldValue;
	}
	/**
	* Stores the current statprefse of an object in this JuppMap, making it available for later retrieval

	@param key The key under which a stored object was previously put

	@param value The object which is to be stored

	@return The object previously stored on this key, or null if no such mapping exists

	@see #get(String)
	*/
	public V put(String key, V value) throws IOException, ClassNotFoundException, BackingStoreException
	{
		V oldValue = get(key);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(value);
    oos.close();
		byte[] ab = putBytes(baos.toByteArray());
		if (ab.length < MAX_VALUE_LENGTH)
			NODE.putByteArray(key, ab);
		else
		{
			Preferences child = NODE.node(key);
			int arrayLength = ab.length;
			int i = 0;
			for (int start = 0; start < arrayLength; start += MAX_VALUE_LENGTH)
			{
				int end = start + MAX_VALUE_LENGTH;
				if (end > arrayLength)
					end = arrayLength;
				child.putByteArray(Integer.toString(i++), Arrays.copyOfRange(ab, start, end));
			}
		}
		NODE.sync();
		return oldValue;
	}
	/**
	* Override this method to apply a transformation to the byte array
	* that will be stored by the back-end. By default, this method
	* returns the byte[] reference that was supplied as an argument (it
	* does nothing to the byte array).
	*
	* The expected use of this method is to allow compression to be
	* applied to an object before it is stored.
	*
	* @param bytes The array of bytes that represents the object to be
	* stored by the put method
	*
	* @return A transformed array of bytes that will be stored in the
	* storage back-end of this JuppMap. By default this method returns
	* the array of bytes un-modified.
	*
	* @see #getBytes(byte[])
	* @see #put(String, Object)
	*
	*/
	protected byte[] putBytes(byte[] bytes) throws IOException
	{ return bytes; }
	/**
	* Override this method to apply a transformation to the byte array
	* returned from the back-end. By default, this method
	* returns the byte[] reference that was supplied as an argument (it
	* does nothing to the byte array).
	*
	* The expected use of this method is to allow compression to be
	* reversed before the get method attempts to read an object from the
	* byte array
	*
	* @param bytes The array of bytes retrieved from the storage back-end,
	* which were previously transformed by putBytes(byte[])
	*
	* @return A transformed array of bytes from which the get method will
	* attempt to de-serialize an object. By default this method returns
	* the array of bytes un-modified.
	*/
	protected byte[] getBytes(byte[] bytes) throws IOException
	{ return bytes; }
	/**
	* Returns the names of known JuppMaps in this user's Preferences store.

	@return A list of strings, in no particular order, of previously-created JuppMaps.
	*/
	public static List<String> getMapNames() throws BackingStoreException
	{
		ArrayList<String> names = new ArrayList<String>();
		for (String name : PREFS.childrenNames())
			if (name.startsWith(MAP_NAME_PREFIX))
				names.add(name.substring(MAP_NAME_PREFIX.length()));
		return names;
	}
	/**
	* Returns the names of existing keys in this JuppMap.

	@return A list of strings, in no particular order, of previously-put objects in the JUppMap.
	*/
	public List<String> getKeys() throws BackingStoreException
	{
		ArrayList<String> keys = new ArrayList<String>();
		for (String key : NODE.keys())
			keys.add(key);
		for (String key : NODE.childrenNames())
			keys.add(key);
		return keys;
	}
	/**
	* Clear all keys and values in this JuppMap
	*/
	public void clear() throws BackingStoreException
	{
		NODE.clear();
		for (String s : NODE.childrenNames())
			NODE.node(s).removeNode();
		NODE.sync();
	}
	/**
	* Attempts to remove traces of this JuppMap from the Preferences store
	*/
	public void destroy() throws BackingStoreException
	{
		NODE.removeNode();
		NODE.flush();
	}
	/** Returns a set of key-value mappings for this JuppMap.
	* <p>
	* The Map.Entry objects are constructed with the key for the related entry, but the
	* object returned by getValue() may not be restored until getValue() is invoked.
	* Note that in order to implement the Map.Entry interface, the Map.Entry
	* implementation used by JuppMap does *not* throw exceptions if the Preferences
	* subsystem throws an Exception while restoring the object for getValue()
	* </p>
	*/
	public Set<Map.Entry<String, V>> entrySet() throws BackingStoreException, ClassNotFoundException, IOException
	{
		Set<Map.Entry<String, V>> entries = new HashSet<Map.Entry<String, V>>();
		for (String key : getKeys())
			entries.add(new JuppEntry(key));
		return entries;
	}
	private class JuppEntry implements Map.Entry<String, V>
	{
		private String key;
		public JuppEntry(String key)
		{
			this.key = key;
		}
		@Override
		public V setValue(V value)
		{
			try
			{
				return JuppMap.this.put(key, value);
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
			return null;
		}
		@Override
		public V getValue()
		{
			try
			{
				return JuppMap.this.get(key);
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
			return null;
		}
		@Override
		public String getKey()
		{
			return key;
		}
	}
}
